/*
 * Copyright 2014 Baidu, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package com.maonings.baidusdk.util;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

import com.maonings.baidusdk.BceClientException;

/**
 * Used to perform length check to ensure the number of bytes read from the underlying input stream is the same as the
 * expected total.
 */
public class LengthCheckInputStream extends FilterInputStream {
    public static final boolean INCLUDE_SKIPPED_BYTES = true;
    public static final boolean EXCLUDE_SKIPPED_BYTES = false;
    /**
     * Total number of bytes expected to be read from the underlying input stream.
     */
    private final long expectedLength;
    /**
     * True if skipped bytes are to be included as part of the data length; false otherwise.
     */
    private final boolean includeSkipped;
    /**
     * The length of the data read from the underlying input stream so far.
     */
    private long dataLength;
    private long marked; // used for mark-and-reset purposes

    /**
     * Constructs an input stream that performs length check to ensure the number of bytes read from the underlying
     * input stream is the same as the expected total.
     *
     * @param in             the underlying input stream
     * @param expectedLength the total length of the data in bytes expected to be read from the underlying input stream;
     *                       must be non-negative.
     * @param includeSkipped true if bytes skipped are to be considered as part of the data length; false otherwise.
     *                       Typically, this parameter should be set to false for uploading data to BCE,
     *                       but set to true for receiving data from BCE.
     */
    public LengthCheckInputStream(InputStream in, long expectedLength, boolean includeSkipped) {
        super(in);
        if (expectedLength < 0) {
            throw new IllegalArgumentException();
        }
        this.expectedLength = expectedLength;
        this.includeSkipped = includeSkipped;
    }

    /**
     * {@inheritDoc}
     *
     * @throws com.maonings.baidusdk.BceClientException if the data length read has exceeded the expected total, or if the total
     *                                         data length is not the same as the expected total.
     */
    @Override
    public int read() throws IOException {
        final int c = super.read();
        if (c >= 0) {
            this.dataLength++;
        }
        this.checkLength(c == -1);
        return c;
    }

    /**
     * {@inheritDoc}
     *
     * @throws com.maonings.baidusdk.BceClientException if the data length read has exceeded the expected total, or if the total
     *                                         data length is not the same as the expected total.
     */
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int readLen = super.read(b, off, len);
        this.dataLength += readLen >= 0 ? readLen : 0;
        this.checkLength(readLen == -1);
        return readLen;
    }

    @Override
    public void mark(int readlimit) {
        super.mark(readlimit);
        this.marked = this.dataLength;
    }

    @Override
    public void reset() throws IOException {
        super.reset();
        if (super.markSupported()) {
            this.dataLength = this.marked;
        }
    }

    /**
     * Checks the data length read so far against the expected total.
     *
     * @param eof true if end of stream has been encountered; false otherwise
     * @throws com.maonings.baidusdk.BceClientException if the data length read has exceeded the expected total, or if the total
     *                                         data length is not the same as the expected total.
     */
    private void checkLength(boolean eof) {
        if (eof) {
            if (this.dataLength != this.expectedLength) {
                throw new BceClientException("Data read (" + this.dataLength +
                        ") has a different length than the expected (" + this.expectedLength +
                        ")");
            }
        } else if (this.dataLength > this.expectedLength) {
            throw new BceClientException("More data read (" + this.dataLength + ") than expected (" +
                    this.expectedLength + ")");
        }
    }

    /**
     * {@inheritDoc}
     *
     * @throws com.maonings.baidusdk.BceClientException if {@link #includeSkipped} is true and the data length skipped has
     *                                         exceeded the expected total.
     */
    @Override
    public long skip(long n) throws IOException {
        final long skipped = super.skip(n);
        if (this.includeSkipped && skipped > 0) {
            this.dataLength += skipped;
            this.checkLength(false);
        }
        return skipped;
    }
}
